﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Migrator.Packets;
using Migrator.Services.Advertisement.Packets;
using Migrator.Util;
using Migrator.Entities;
using Migrator.Core;
using Migrator.Services.Registration.Packets;
using System.Diagnostics;
using Migrator.Services.RouteOptimization.Packets;

namespace Migrator.Services.MobileIPNodeService
{
    /// <summary>
    /// Mobile IP service for mobile nodes
    /// </summary>
    public class MobileIPNodeService : IService
    {
        public MobileIPNodeService(Node client)
        {
            this.Client = client;
            this.Client.LocationChanged += new Node.LocationChangedEventHandler(Client_LocationChanged);
            this.solicitor = new Solicitor(client);
            this.registerer = new Registerer(client);
            this.advertisements = new Dictionary<MACAddress, KeyValuePair<MobileIPAdvertisementPacket, int>>();

            // Necessary in order to discover the node's home agent
            this.Client.DefaultRouterChanged += new Node.DefaultRouterChangedEventHandler(Client_DefaultRouterChanged);
        }

        public void Update()
        {
            // Home agent begins with value null
            if (this.homeAgent == null)
                return;

            this.solicitor.Update();

            this.registerer.PendingRegistrations.RemoveExpired(Simulator.Instance.Clock);

            // Move detection
            // If I need to register...
            if (this.NeedToRegister())
            {
                // Choose one of the received advertisements and register
                // with that agent
                if (!this.ChooseAdvertisementAndRegister())
                {
                    // Can't find any valid advertisements? Make a solicitation
                    if (solicitor.CanSolicitate())
                    {
                        solicitor.Solicitate();
                    }
                }
            }
        }

        public bool Filter(IPPacket packet)
        {
            return (packet is MobileIPAdvertisementPacket) || (packet is RegistrationReplyPacket);
        }

        public void Handle(IPPacket packet)
        {
            MobileIPAdvertisementPacket mobilePacket = packet as MobileIPAdvertisementPacket;
            if (mobilePacket != null)
                this.Handle(mobilePacket);

            RegistrationReplyPacket replyPacket = packet as RegistrationReplyPacket;
            if (replyPacket != null)
                this.Handle(replyPacket);
        }

        public IService Configure(params object[] configuration)
        {
            this.Enabled = true;
            return this;
        }

        public bool Enabled
        {
            get;
            set;
        }

        public Node Client
        {
            get;
            private set;
        }

        public RegistrationsTable.Entry CurrentRegistration
        {
            get { return this.currentRegistration; }
        }

        #region Internal methods

        /// <summary>
        /// Node has moved: clear advertisement history and make a solicitation
        /// </summary>
        /// <param name="source">The node that has moved</param>
        /// <param name="oldLocation">Its old position</param>
        private void Client_LocationChanged(Node source, System.Drawing.PointF oldLocation)
        {
            this.advertisements.Clear();
            this.willRegisterAheadOfTime = global::Migrator.Properties.Settings.Default.WillRegisterOnMove;
        }

        private void Handle(MobileIPAdvertisementPacket packet)
        {
            // If the advertiser provides the foreign agent service, and is
            // not busy, let's record this advertisement.
            // Also, record home agent advertisements
            if (packet.F && !packet.B || packet.Source == this.homeAgent)
            {
                this.advertisements[packet.Source.MACAddress] = new KeyValuePair<MobileIPAdvertisementPacket, int>(packet, packet.LifeTime + Simulator.Instance.Clock);
            }

            // If the advertiser is my home agent and I'm registered to
            // some foreign agent...
            if (packet.Source == this.homeAgent && this.currentRegistration != null)
            {
                // return home
                this.Register(packet.Source, this.currentRegistration.CareOfAddress, 0);
            }
            else if (this.currentRegistration != null && 
                packet.Source.MACAddress == this.currentRegistration.ForeignAgent.MACAddress)
            {
                // Check sequence numbers
                if (packet.SequenceNumber < foreignAgentLastSeqNumber && packet.SequenceNumber < 256)
                {
                    this.Register(packet.Source, packet.CareOfAddresses.First(), packet.RegistrationLifeTime);
                }
            }

            this.willRegisterAheadOfTime = false;
        }

        private void Handle(RegistrationReplyPacket packet)
        { 
            if (this.registerer.PendingRegistrations.Contains(packet.SourceIP))
            {
                var entry = this.registerer.PendingRegistrations[packet.SourceIP];

                // If the registration was accepted
                if (packet.Code < 10)
                {
                    Node previousForeignAgent = null;

                    // If the route optimization feature is enabled, warn my previous
                    // foreign agent about my new registration
                    if (Migrator.Properties.Settings.Default.EnableRouteOptimization)
                    {
                        if (this.currentRegistration != null)
                        {
                            previousForeignAgent = this.currentRegistration.ForeignAgent;
                        }
                    }
                    this.currentRegistration = (packet.Lifetime == 0) ? null 
                                                                      : new RegistrationsTable.Entry(entry.ForeignAgent, 
                                                                                                     entry.DestinationIP, 
                                                                                                     entry.CareOfAddress, 
                                                                                                     packet.Lifetime + Simulator.Instance.Clock, 
                                                                                                     entry.RequestedLifetime);
                    this.Client.DefaultRouter = (Router)packet.Source;

                    // Warn the previous foreign-agent
                    if (previousForeignAgent != null && (this.currentRegistration == null || previousForeignAgent != this.currentRegistration.ForeignAgent))
                    {
                        if (Migrator.Properties.Settings.Default.EnableRouteOptimization)
                        {
                            this.Client.Send(this.InitializeBindingUpdate(previousForeignAgent.IPAddress));
                        }
                    }
                }

                this.registerer.PendingRegistrations.Remove(packet.SourceIP);
                this.willRegisterAheadOfTime = false;
            }
        }

        private bool HasExpired(MACAddress advertiser)
        {
            return !this.advertisements.ContainsKey(advertiser) || Simulator.Instance.Clock > this.advertisements[advertiser].Value;
        }

        private bool NeedToRegister()
        {
            if (this.homeAgent == null)
                return false;

            if (this.willRegisterAheadOfTime)
            {
                //this.willRegisterAheadOfTime = false;
                return true;
            }

            if (!HasExpired(this.homeAgent.MACAddress))
                return false;

            if (this.currentRegistration == null)
                return true;

            if (HasExpired(this.currentRegistration.ForeignAgent.MACAddress))
                return true;

            // If my current registration is going to expire in less than 5 seconds...
            if (Simulator.Instance.Clock + 5 > this.currentRegistration.ExpirationTime)
                return true;

            return false;
        }

        private bool ChooseAdvertisementAndRegister()
        {
            foreach (var macAddress in this.advertisements.Keys)
            {
                if (!HasExpired(macAddress))
                {
                    var packet = this.advertisements[macAddress].Key;

                    // If I'm not waiting for a reply from this foreign agent...
                    if (!this.registerer.PendingRegistrations.Contains(packet.SourceIP))
                    {
                        this.Register(packet.Source, packet.CareOfAddresses.First(), packet.RegistrationLifeTime);
                        return true;
                    }
                }
            }

            return false;
        }

        private void Register(Node foreignAgent, IPAddress careOfAddress, ushort requestedLifetime)
        {
            if (this.preferCoLocatedAddress)
            {
                // TODO Acquire an address through DHCP and then register
            }
            else
            {
                this.registerer.Register(this.homeAgent,
                                         foreignAgent,
                                         careOfAddress,
                                         false,
                                         true,
                                         this.preferReceiveBroadcastPackets,
                                         this.preferSimultaneousBindings,
                                         requestedLifetime);

            }
        }

        /// <summary>
        /// The home agent is the first default router of the node
        /// obtained through DHCP
        /// </summary>
        void Client_DefaultRouterChanged(Node source, Router oldRouter)
        {
            if (this.homeAgent == null)
            {
                this.homeAgent = source.DefaultRouter;
            }
        }

        private IPPacket InitializeBindingUpdate(IPAddress targetAddress)
        {
            ushort remainingLifetime = 0;

            IPAddress careOfAddress = this.Client.IPAddress;

            if (this.currentRegistration != null)
            {
                remainingLifetime = (ushort)(this.currentRegistration.ExpirationTime - Simulator.Instance.Clock);
                careOfAddress = this.currentRegistration.CareOfAddress;
            }

            BindingUpdatePacket packet = new BindingUpdatePacket(this.Client, targetAddress);
            packet.Lifetime = remainingLifetime;
            packet.HomeAddress = this.Client.IPAddress;
            packet.CareOfAddress = careOfAddress;

            return packet;
        }

        #endregion

        private Solicitor solicitor;
        private Registerer registerer;
        private Dictionary<MACAddress, KeyValuePair<MobileIPAdvertisementPacket, int>> advertisements;
        private Node homeAgent;
        private int foreignAgentLastSeqNumber = 0;
        private RegistrationsTable.Entry currentRegistration = null;
        private bool willRegisterAheadOfTime = false;

        /*
         * MobileIP node preferences 
         */
        private bool preferCoLocatedAddress = false;
        private bool preferReceiveBroadcastPackets = true;
        private bool preferSimultaneousBindings = true;
    }
}
